"""The Game of Pig"""

from dice import make_fair_die, make_test_die
from ucb import main, trace, log_current_line, interact

goal = 100  # The goal of pig is always to score 100 points.
best_turn_goal = 15
best_four_side_goal = 9
best_margin = 10

# Taking turns
def roll(turn_total, outcome):
    """Performs the roll action, which adds outcome to turn_total, or loses the
    turn on outcome == 1.

    Arguments:
    turn -- number of points accumulated by the player so far during the turn
    outcome -- the outcome of the roll (the number generated by the die)

    Returns three values in order:
    - the number of points the player scores after the roll
      Note: If the turn is not over after this roll, this return value is 0.
            No points are scored until the end of the turn.
    - the player turn point total after the roll
    - a boolean; whether or not the player's turn is over

    >>> roll(7, 3)
    (0, 10, False)
    >>> roll(99, 1)
    (1, 0, True)
    """
    if outcome == 1:
        return  (1,0,True)
    else:
        return (0,turn_total+outcome,False)

def hold(turn_total, outcome):
    """Performs the hold action, which adds turn_total to the player's score.

    Arguments:
    turn -- number of points accumulated by the player so far during the turn
    outcome -- the outcome of the roll, ie. the number generated by the die

    Returns three values in order:
    - the number of points the player scores after holding
    - the player turn total after the roll (always 0)
    - a boolean; whether or not the player's turn is over

    >>> hold(99, 1)
    (99, 0, True)
    """
    return  (turn_total,0,True)

def take_turn(plan, dice=make_fair_die(), who='Someone', comments=False):
    """Simulate a single turn and return the points scored for the whole turn.

    Important: The d function should be called once, **and only once**, for
               every action taken!  Testing depends upon this fact.

    Arguments:
    plan -- a function that takes the turn total and returns an action function
    dice -- a function that takes no args and returns an integer outcome.
            Note: dice is non-pure!  Call it exactly once per action.
    who -- name of the current player
    comments -- a boolean; whether commentary is enabled
    """
    score_for_turn = 0
    over = False
    while( not over):
        action = plan(score_for_turn)
        outcome = dice()
        res_act = action(score_for_turn,outcome)
        over = res_act[2]
        if not over:
            score_for_turn = res_act[1]
        else:
            score_for_turn = res_act[0]
        if comments:
            commentate(action,outcome,score_for_turn,score_for_turn,over,who)
    return score_for_turn

def take_turn_test():
    """Test the take_turn function using deterministic test dice."""
    print("Take turn TEST")
    plan = make_roll_until_plan(10)
    print('Take turn test die:',2,3,2,4)
    print('This turn total:',take_turn(plan,make_test_die(2,3,2,4), comments=False))
    print('Take turn test die:',2,3,1,4)
    print('This turn total:',take_turn(plan,make_test_die(2,3,1,4), comments=False))
    print("Ending Test ...\n")

# Commentating

def commentate(action, outcome, score_for_turn, turn_total, over, who):
    """Print descriptive comments about a game event.

    action -- the action function chosen by the current player
    outcome -- the outcome of the die roll
    score_for_turn -- the points scored in this turn by the current player
    turn_total -- the current turn total
    over -- a boolean that indicates whether the turn is over
    who -- the name of the current player
    """
    print(who, describe_action(action))
    print(draw_number(outcome))
    if over:
        print(who, ' turn score:', score_for_turn,'\n')
    else:
        print(who, ' turn total:', turn_total, '\n')

def describe_action(action):
    """Generate a string that describes an action.

    action -- a function, which should be either hold or roll

    If action is neither the hold nor roll function, the description should
    announce that cheating has occurred.

    >>> describe_action(roll)
    'chose to roll.'
    >>> describe_action('chose to roll.')
    'decided to hold.'
    >>> describe_action(commentate)
    'took an illegal action!'
    """
    assert action == roll or action == hold ,'took an illegal action!'
    return {roll:'rolling...',
            hold:'decided to hold.'}[action]

def draw_number(n, dot='*'):
    """Return an ascii art representation of rolling the number n.

    >>> print(draw_number(5))
     -------
    | *   * |
    |   *   |
    | *   * |
     -------
    """
    return {1 : draw_die(True,False,False,False,dot),
            2 : draw_die(False,True,False,False,dot),
            3 : draw_die(True,False,True,False,dot),
            4 : draw_die(False,True,True,False,dot),
            5 : draw_die(True,True,True,False,dot),
            6 : draw_die(False,True,True,True,dot)}[n]

def draw_die(c, f, b, s, dot):
    """Return an ascii art representation of a die.

    c, f, b, & s are boolean arguments. This function returns a multi-line
    string of the following form, where the letters in the diagram are either
    filled if the corresponding argument is true, or empty if it is false.

     -------
    | b   f |
    | s c s |
    | f   b |
     -------

    Note: The sides with 2 and 3 dots have 2 possible depictions due to
          rotation. Either representation is acceptable.

    Note: This function uses Python syntax not yet covered in the course.

    c, f, b, s -- booleans; whether to place dots in corresponding positions
    dot        -- A length-one string to use for a dot
    """
    border = ' -------'
    def draw(b):
        return dot if b else ' '
    c, f, b, s = map(draw, [c, f, b, s])
    top =    ' '.join(['|', b, ' ', f, '|'])
    middle = ' '.join(['|', s, c,   s, '|'])
    bottom = ' '.join(['|', f, ' ', b, '|'])
    return '\n'.join([border, top, middle, bottom, border])


# Game simulator

def play(strategy, opponent_strategy):
    """Simulate a game and return 0 if the first player wins and 1 otherwise.

    strategy -- The strategy function for the first player (who plays first)
    opponent_strategy -- The strategy function for the second player
    """
    who = 0 # Which player is about to take a turn, 0 (first) or 1 (second)
    score = 0
    opponent_score = 0
    sides = 6
    while score < goal and opponent_score < goal:
        if (score+opponent_score)%7 ==0:
            sides = 4
        if 0 == who:
            plan = strategy(score,opponent_score)
            score += take_turn(plan, dice=make_fair_die(sides),who='You')
        else:
            opponent_plan = opponent_strategy(opponent_score,score)
            opponent_score += take_turn(opponent_plan, dice=make_fair_die(sides),who='Computer')
        who = other(who)
    '''
    if 0 == who:
        print("Computer Win!!!")
    else:
        print("You Win!!!")
    '''
    return who

def other(who):
    """Return the other player, for players numbered 0 and 1.

    >>> other(0)
    1
    >>> other(1)
    0
    """
    return (who + 1) % 2


# Basic Strategies

def make_roll_until_plan(turn_goal=20):
    """Return a plan to roll until turn total is at least turn_goal."""
    def plan(turn):
        if turn >= turn_goal:
            return hold
        else:
            return roll
    return plan

def make_roll_until_strategy(turn_goal):
    """Return a strategy to always adopt a plan to roll until turn_goal.

    A strategy is a function that takes two game scores as arguments and
    returns a plan (which is a function from turn totals to actions).
    """
    def strategy(a,b):
        def plan(n):
            if n >= turn_goal:
                return hold
            else:
                return roll
        return plan
    return strategy

def make_roll_until_strategy_test():
    """Test that make_roll_until_strategy gives a strategy that returns correct
    roll-until plans."""
    strategy = make_roll_until_strategy(15)
    plan = strategy(0, 0)
    assert plan(14) == roll, 'Should have returned roll'
    assert plan(15) == hold, 'Should have returned hold'
    assert plan(16) == hold, 'Should have returned hold'


# Experiments (Phase 2)

def average_value(fn, num_samples,*args):
    """Compute the average value returned by fn over num_samples trials.

    >>> d = make_test_die(1, 3, 5, 7)
    >>> average_value(d, 100)
    4.0
    """
    totle = 0
    for i in range(num_samples):
        totle += fn(*args)
    return totle/num_samples

def averaged(fn, num_samples=100):
    """Return a function that returns the average_value of fn when called.

    Note: To implement this function, you will have to use *args syntax, a new
          Python feature introduced in this project.  See the project
          description for details.

    >>> die = make_test_die(3, 1, 5, 7)
    >>> avg_die = averaged(die)
    >>> avg_die()
    4.0
    >>> avg_turn = averaged(take_turn)
    >>> avg_turn(make_roll_until_plan(4), die, 'The player', False)
    3.0

    In this last example, two different turn scenarios are averaged.
    - In the first, the player rolls a 3 then a 1, receiving a score of 1.
    - In the other, the player rolls a 5 (then holds on the 7), scoring 5.
    Thus, the average value is 3.0

    Note: If this last test is called with comments=True in take_turn, the
    doctests will fail because of the extra output.
    """
    def avg_fn(*args):
        return average_value(fn,num_samples,*args)
    return avg_fn

def compare_strategies(strategy, baseline=make_roll_until_strategy(20)):
    """Return the average win rate (out of 1) of strategy against baseline."""
    as_first = 1 - averaged(play)(strategy, baseline)
    as_second = averaged(play)(baseline, strategy)
    return (as_first + as_second) / 2  # Average the two results

def eval_strategy_range(make_strategy, lower_bound, upper_bound):
    """Return the best integer argument value for make_strategy to use against
    the roll-until-20 baseline, between lower_bound and upper_bound (inclusive).

    make_strategy -- A one-argument function that returns a strategy.
    lower_bound -- lower bound of the evaluation range
    upper_bound -- upper bound of the evaluation range
    """
    best_value, best_win_rate = 0, 0
    value = lower_bound
    while value <= upper_bound:
        strategy = make_strategy(value)
        win_rate = compare_strategies(strategy)
        print(value, 'win rate against the baseline:', win_rate)
        if win_rate > best_win_rate:
            best_win_rate, best_value = win_rate, value
        value += 1
    return best_value

def run_strategy_experiments():
    """Run a series of strategy experiments and report results."""
    print("The turn goal TEST:")
    print("The turn_goal",eval_strategy_range(make_roll_until_strategy,15,25),"strategy is best!\n")

    print("The four side goal TEST:")
    print("The four_side_goal",eval_strategy_range(make_die_specific_strategy,5,15),"strategy is best!\n")

    print("The margin TEST:")
    print("The margin",eval_strategy_range(make_pride_strategy,0,10),"strategy is best!\n")

    print("My finsl strategy TEST:")
    print("The win rate of my final strategy:",compare_strategies(final_strategy),"\n")

def make_die_specific_strategy(four_side_goal, six_side_goal=20):
    """Return a strategy that returns a die-specific roll-until plan.

    four_side_goal -- the roll-until goal whenever the turn uses a 4-sided die
    six_side_goal -- the roll-until goal whenever the turn uses a 6-sided die

    """
    def strategy(a,b):
        if (a+b)%7 == 0:
            turn_goal = four_side_goal
        else:
            turn_goal = six_side_goal
        return make_roll_until_strategy(turn_goal)(a,b)
    return strategy

def make_pride_strategy(margin, turn_goal=20):
    """Return a strategy that wants to finish a turn winning by at least margin.

    margin -- the size of the lead that the player requires
    turn_goal -- the minimum roll-until turn goal, even when winning
    """
    def strategy(a,b):
        def plan(n):
            if n >= turn_goal or a+n > b+margin:
                return hold
            else:
                return roll
        return plan
    return strategy

def final_strategy(score, opponent_score):
    """Return a plan according to my final strategy.

    Strategy:
    Hold before a four_side dice so that the opponent easily get a pig('1').
    Hold when the turn score is not less than the  best_turn_goal.
    Hold if your score after holding will be at least best_margin greater than the opponent.

    Note:The best_turn_goal and best_margin are decided according to a lot tests,
         They are defined at the start of this file.
    """
    def plan(n):
        if (score+opponent_score)%7 == 0 or n>= best_turn_goal or score+n > opponent_score+best_margin:
            return hold
        else:
            return roll
    return plan

def interactive_strategy(score, opponent_score):
    """Prints total game scores and returns an interactive plan.

    Note: this function uses Python syntax not yet covered in the course.
    """
    print('You:', score, ' Computer:', opponent_score, '(total score)')
    def plan(turn):
        if turn > 0:
            print('You    turn total:', turn)
        while True:
            response = input('(R)oll or (H)old?')
            if response.lower()[0] == 'r':
                return roll
            elif response.lower()[0] == 'h':
                return hold
            print('Huh?')
    return plan

@main
def run():
    take_turn_test()

    #uncomment and set default comments to True to play
    #play(interactive_strategy, make_roll_until_strategy(20))

    make_roll_until_strategy_test()

    run_strategy_experiments()

